﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Globalization;
using System.Runtime.Serialization;
using coord = System.Double;
using System.IO;

namespace GeoRef
{    
    /// <summary>
    /// Represents a single geographic point
    /// </summary>
    [DataContract]
    public class GeoPoint
    {
        #region GeoPoint fields

        /// <value>
        /// Latitude
        /// </value>
        [DataMember]
        private coord lat;

        /// <value>
        /// Longitude
        /// </value>
        [DataMember]
        private coord lng;

        #endregion

        #region GeoPoint Constructors
        /// <summary>
        /// Initializes a new instance of the GeoPoint class,
        /// specified by a pair of double
        /// </summary>
        /// <param name="latitude">Latitude</param>
        /// <param name="longitude">Longitude</param>
        /// <remarks>
        /// Latitude must have a value between -90.0 and 90.0 
        /// while longitude must be between -180.0 and 180.0
        /// </remarks>
        /// <exception cref="System.ArgumentOutOfRangeException">
        /// If latitude and/or longitude are out of range
        /// </exception>
        /// <exception cref="System.OverflowException">
        /// If latitude and/or longitude are out of range
        /// of doubles
        /// </exception>
        public GeoPoint(coord latitude, coord longitude) {
            this.TestRange(latitude, longitude);
            this.RoundCoordinates();
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="GeoPoint" /> class,
        /// specifying coordinates in string representation
        /// </summary>
        /// <param name="coordinates">Coordinates composed
        /// of two double precision floating point separeted
        /// by comma</param>
        /// <remarks>
        /// Latitude must have a value between -90.0 and 90.0 
        /// while longitude must be between -180.0 and 180.0
        /// </remarks>
        /// <example>
        /// <code>
        /// GeoPoint myPos = new GeoPoint("-43.716667,23.4");
        /// </code>
        /// </example>
        /// <exception cref="System.ArgumentOutOfRangeException">
        /// If latitude and/or longitude are out of range
        /// </exception>
        /// <exception cref="System.FormatException">
        /// If coordinates have a wrong format (e.g letters)
        /// or something different from ("lat,lng")
        /// </exception>
        /// <exception cref="System.NullReferenceException">
        /// If coordinates argument is a null reference
        /// </exception>
        /// <exception cref="System.OverflowException">
        /// If latitude and/or longitude are out of range
        /// of doubles
        /// </exception>
        public GeoPoint(string coordinates) {                        
            string[] tmp;
            coord tmpLa, tmpLo;
            if (coordinates.Contains(",") == true) {
                tmp = coordinates.Split(',');
                tmpLa = System.Double.Parse(tmp[0], CultureInfo.InvariantCulture);
                tmpLo = System.Double.Parse(tmp[1], CultureInfo.InvariantCulture);
                this.TestRange(tmpLa, tmpLo);
            } else {
                throw new System.FormatException("Wrong parameter format ");
            }
            this.RoundCoordinates();
        }

        #endregion

        #region GeoPoint Properties
        /// <summary>
        /// Gets or sets latitude property
        /// </summary>
        /// <exception cref="System.ArgumentOutOfRangeException">
        /// If latitude is not between -90 and 90
        /// </exception>
        public coord Lat{
            set {TestRange(value, this.lng);}
            get {return lat;}
        }

        /// <summary>
        /// Gets or sets longitude property
        /// </summary>
        /// <exception cref="System.ArgumentOutOfRangeException">
        /// If longitude is not between -180 and 180
        /// </exception>
        public coord Lng {
            set { TestRange(this.lat, value); }
            get {return lng;}
        }

        #endregion

        #region GeoPoint Private Method

        /// <summary>
        /// Checks range of both latitude and longitude
        /// </summary>
        /// <param name="lat">Latitude</param>
        /// <param name="lng">Longitude</param>
        /// <exception cref="System.ArgumentOutOfRangeException">
        /// If latitude is not between -90 and 90, or longitude is
        /// not between -180 and 180</exception>
        private void TestRange (coord lat, coord lng) {
            if ((lat <= 90 && lat >= -90) &&
               (lng <= 180 && lng >= -180)) {
                    this.lat = lat;
                    this.lng = lng;
            } else {
                throw new System.ArgumentOutOfRangeException("Wrong Latitude and/or Longitude");
            }
        }


        /// <summary>
        /// Rounds each digit of coordinates to the fifth floating digit
        /// </summary>
        protected void RoundCoordinates() {
            this.Lat = Math.Round(this.Lat, 5, MidpointRounding.ToEven);
            this.Lng = Math.Round(this.Lng, 5, MidpointRounding.ToEven);
        }

        #endregion

        #region GeoPoint Public Methods

        /// <summary>
        /// Determines if two GeoPoints have equal value
        /// </summary>
        /// <param name="p1">first <see cref="GeoPoint" /> to compare</param>
        /// <param name="p2">second <see cref="GeoPoint" /> to compare</param>
        /// <returns>True if p1 is equal to p2, false otherwise</returns>
        /// <exception cref="ArgumentNullException">If first and/or second
        /// point are null</exception>
        public static bool operator ==(GeoPoint p1, GeoPoint p2)
        {
            try { bool tmp = (p1.Lat == 0); }
            catch (NullReferenceException)
            {
                try { bool tmp = p2.Lat == 0; }
                catch (NullReferenceException)
                {
                    // p1==null && p2 ==null
                    return true;
                }
                // p1==null && p2 !=null
                return false;
            }

            try
            {
                if (p1.lat == p2.lat && p1.lng == p2.lng)
                    return true;
                else
                    return false;
            }
            catch (NullReferenceException)
            {
                // p1 != null && p2 == null
                return false;
            }
        }
        /// <summary>
        ///  Determines if two GeoPoints have different value
        /// </summary>
        /// <param name="p1">first <see cref="GeoPoint" /> to compare</param>
        /// <param name="p2">second <see cref="GeoPoint" /> to compare</param>
        /// <returns>True if p1 is different to p2, false otherwise</returns>
        /// <exception cref="ArgumentNullException">If first and/or second
        /// point are null</exception>
        public static bool operator !=(GeoPoint p1, GeoPoint p2) {
            return !(p1 == p2);
        }

        /// <summary>
        /// Determines whether this instance of <see cref="GeoPoint" /> and a
        /// specificed object obj, which must also be a <see cref="GeoPoint" />
        /// object, have the same value
        /// </summary>
        /// <param name="obj">Object to compare</param>
        /// <returns>True if this is equal to obj, false otherwise</returns>
        public override bool Equals(object obj) {
            if (!(obj is GeoPoint)) {
                return false;
            } else {
                return this == (GeoPoint)obj;
            }
        }

		/// <summary>
		/// Returns the default hash code
		/// </summary>
		/// <returns>
		/// An <see cref="System.Int32"/>
		/// </returns>
		public override int GetHashCode ()
		{
			return base.GetHashCode();
		}

		
        /// <summary>
        /// Converts latitude and longitude in string format separated by comma
        /// </summary>
        /// <returns>A string representing coordinates</returns>
        public override string ToString() {
            return string.Format(CultureInfo.InvariantCulture, "{0:G},{1:G}", lat, lng);
        }

        #endregion
    }
}
